
// ##################
// initialises web GL and starts the render loop
function webGLStart() {

	// get canvas
	canvas = document.getElementById("Canvas3D");
	
	var glInit = initGL(canvas);
	var shInit = initShaders();
	var buInit = initBuffers();
	
	// if error, don't start render loop
	if(!glInit || !shInit || !buInit) {
		return false;
	}

	gl.clearColor(0.0, 0.0, 0.0, 1.0);
	gl.enable(gl.DEPTH_TEST);

	tick();
}

// ##################
// get gl object and set it's viewport dimensions
function initGL(canvas) {
	try {
		// set up WebGL
		gl = WebGLUtils.setupWebGL(canvas, {antialias: true});
		gl.viewportWidth = canvas.width;
		gl.viewportHeight = canvas.height;
		
		if(!gl) {
			throw new Error("WebGLUtils.setupWebGL failed");
		}
		
	} catch (e) {
		alert("Could not initialise WebGL, sorry :-(. "+ e.message);
		return false;
	}
	
	return true;
}

// ##################
// get shader from gl for shader source given in element with id given
function getShader(gl, id) {
	var shaderScript = document.getElementById(id);
	if (!shaderScript) {
		return null;
	}

	var str = "";
	var k = shaderScript.firstChild;
	while (k) {
		if (k.nodeType == 3) {
			str += k.textContent;
		}
		k = k.nextSibling;
	}

	var shader;
	if (shaderScript.type == "x-shader/x-fragment") {
		shader = gl.createShader(gl.FRAGMENT_SHADER);
	} else if (shaderScript.type == "x-shader/x-vertex") {
		shader = gl.createShader(gl.VERTEX_SHADER);
	} else {
		return null;
	}

	gl.shaderSource(shader, str);
	gl.compileShader(shader);

	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
		alert(gl.getShaderInfoLog(shader));
		return null;
	}

	return shader;
}


var shaderProgram;

// ##################
// inits shaders
function initShaders() {
	// make shaders based on the two definitions
	var fragmentShader = getShader(gl, "shader-fs");
	var vertexShader = getShader(gl, "shader-vs");

	// set up shaders
	shaderProgram = gl.createProgram();
	gl.attachShader(shaderProgram, vertexShader);
	gl.attachShader(shaderProgram, fragmentShader);
	gl.linkProgram(shaderProgram);

	if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
		alert("Could not initialise shaders");
		return false;
	}

	gl.useProgram(shaderProgram);

	shaderProgram.vertexPositionAttribute = gl.getAttribLocation(shaderProgram, "aVertexPosition");
	gl.enableVertexAttribArray(shaderProgram.vertexPositionAttribute);
	
	shaderProgram.vertexColorAttribute = gl.getAttribLocation(shaderProgram, "aVertexColor");
	gl.enableVertexAttribArray(shaderProgram.vertexColorAttribute);

	shaderProgram.pMatrixUniform = gl.getUniformLocation(shaderProgram, "uPMatrix");
	shaderProgram.mvMatrixUniform = gl.getUniformLocation(shaderProgram, "uMVMatrix");
	
	return true;
}


var mvMatrix = mat4.create();
var mvMatrixStack = [];
var pMatrix = mat4.create();

// ##################
// push new model view matrix onto stack
function mvPushMatrix() {
	var copy = mat4.create();
	mat4.set(mvMatrix, copy);
	mvMatrixStack.push(copy);
}

// ##################
// pop current model view matrix off stack
function mvPopMatrix() {
	if (mvMatrixStack.length == 0) {
		throw "Invalid popMatrix!";
	}
	mvMatrix = mvMatrixStack.pop();
}

// ##################
function setMatrixUniformsInShader_shared() {
	gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, pMatrix);
}

// ##################
function setMatrixUniformsInShader_notShared() {
	gl.uniformMatrix4fv(shaderProgram.mvMatrixUniform, false, mvMatrix);
}
	
// ##################
function degToRad(degrees) {
	return degrees * Math.PI / 180;
}


var pyramidVertexPositionBuffer;
var pyramidVertexColorBuffer;
var cubeVertexPositionBuffer;
var cubeVertexColorBuffer_yellow;
var cubeVertexColorBuffer_red;
var cubeVertexColorBuffer_brightRed;
var cubeVertexIndexBuffer;

// ##################
// init the buffers
function initBuffers() {
	pyramidVertexPositionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(getVertices_Pyramid()), gl.STATIC_DRAW);
	pyramidVertexPositionBuffer.itemSize = 3;
	pyramidVertexPositionBuffer.numItems = 12;

	pyramidVertexColorBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
	var colors = [
		// Front face
		1.0, 0.0, 0.0, 1.0,
		0.0, 1.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,

		// Right face
		1.0, 0.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,
		0.0, 1.0, 0.0, 1.0,

		// Back face
		1.0, 0.0, 0.0, 1.0,
		0.0, 1.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,

		// Left face
		1.0, 0.0, 0.0, 1.0,
		0.0, 0.0, 1.0, 1.0,
		0.0, 1.0, 0.0, 1.0
	];
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	pyramidVertexColorBuffer.itemSize = 4;
	pyramidVertexColorBuffer.numItems = 12;

	cubeVertexPositionBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(getVertices_Cube()), gl.STATIC_DRAW);
	cubeVertexPositionBuffer.itemSize = 3;
	cubeVertexPositionBuffer.numItems = 24;

	// yellow
	cubeVertexColorBuffer_yellow = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_yellow);
	colors = generateCubeColors([1.2, 1.0, 0.5]);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	cubeVertexColorBuffer_yellow.itemSize = 4;
	cubeVertexColorBuffer_yellow.numItems = 24;

	// red
	cubeVertexColorBuffer_red = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_red);
	colors = generateCubeColors([1.2, 0.55, 0.5]);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	cubeVertexColorBuffer_red.itemSize = 4;
	cubeVertexColorBuffer_red.numItems = 24;
	
	// bright red
	cubeVertexColorBuffer_brightRed = gl.createBuffer();
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_brightRed);
	colors = generateCubeColors([0.55, 1.2, 0.5]);
	gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
	cubeVertexColorBuffer_brightRed.itemSize = 4;
	cubeVertexColorBuffer_brightRed.numItems = 24;
	
	cubeVertexIndexBuffer = gl.createBuffer();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	var cubeVertexIndices = [
		0,   1,  2,      0,  2,  3, // Front face
		4,   5,  6,      4,  6,  7, // Back face
		8,   9, 10,      8, 10, 11, // Top face
		12, 13, 14,     12, 14, 15, // Bottom face
		16, 17, 18,     16, 18, 19, // Right face
		20, 21, 22,     20, 22, 23  // Left face
	];
	gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(cubeVertexIndices), gl.STATIC_DRAW);
	cubeVertexIndexBuffer.itemSize = 1;
	cubeVertexIndexBuffer.numItems = 36;
	
	return true;
}

var rotationPyramid = 0;
var rotationCube = 0;

// ##################
// draw scene
function drawScene() {

	var profileStart;
	if(testIsProfiling() == true) {
		profileStart = new Date().getTime();
	}

	gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
	
	gl.clear(
		gl.COLOR_BUFFER_BIT
		| gl.DEPTH_BUFFER_BIT);

	// set perspective
	mat4.perspective(
		45,                                   // vertical field of view (in degrees)
		gl.viewportWidth / gl.viewportHeight, // width:height ratio of canvas
		0.1,                                  // near clip (in units from viewpoint)
		10000.0,                              // far clip  (in units from viewpoint)
		pMatrix);                             // projection matrix

	// --------------
	// center mv matrix
	mat4.identity(mvMatrix);                       // identity matrix

	// --------------
	// push for pyramid and cube position
	mvPushMatrix();
	mat4.translate(mvMatrix, [7.0, 4.0, -16.0]);   // translate pyramid and cube location

	// --------------
	// pyramid
	
	mvPushMatrix();
	
	mat4.rotate(mvMatrix, degToRad(rotationPyramid), [0, 1, 0]); // rotate pyramid around y axis

	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, pyramidVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, pyramidVertexColorBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, pyramidVertexColorBuffer.itemSize, gl.FLOAT, false, 0, 0);

	setMatrixUniformsInShader_shared();
	setMatrixUniformsInShader_notShared();
//	gl.drawArrays(gl.TRIANGLES, 0, pyramidVertexPositionBuffer.numItems);
	gl.drawArrays(gl.LINE_LOOP, 0, pyramidVertexPositionBuffer.numItems);

	mvPopMatrix();

	// --------------
	// cube

	mat4.translate(mvMatrix, [3.0, 0.0, 0.0]);    // move position x += 3

	mvPushMatrix();
	mat4.rotate(mvMatrix, degToRad(rotationCube), [1, 1, 1]); // rotate cube on all axis

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexColorBuffer_yellow);
	gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, cubeVertexColorBuffer_yellow.itemSize, gl.FLOAT, false, 0, 0);

	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);
	setMatrixUniformsInShader_shared();
	setMatrixUniformsInShader_notShared();
	gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

	mvPopMatrix();

	// --------------
	// pop back to indentity
	mvPopMatrix();

	// --------------
	// push, position, and rotate for graph x=0, y=0 position
	mvPushMatrix();
	mat4.translate(mvMatrix, INITIAL_GRAPH_XYZ_TRANSLATION); // translate
	mat4.rotate(mvMatrix, degToRad(INITIAL_GRAPH_X_ROTATION), [1, 0, 0]); // rotate graph so looking down on it
	mat4.rotate(mvMatrix, degToRad(INITIAL_GRAPH_Y_ROTATION), [0, -1, 0]); // rotate graph so looking along it

	if(testIsProfiling()) {
		addProfileData('drawScene - animateds',new Date().getTime() - profileStart);
	}
	if(testIsProfiling() == true) {
		profileStart = new Date().getTime();
	}
	
	// --------------
	// draw graph data

	// set vertex position buffer only once, as it is reused for all cubes drawn in the graph.
	gl.bindBuffer(gl.ARRAY_BUFFER, cubeVertexPositionBuffer);
	gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, cubeVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

	var isSelectedOnXAxis = false;
	var isSelectedOnZAxis = false;
	
	// keeps track of colours of the current and previous data points drawn.
	var prevDataPointColourBuffer = null;
	var currDataPointColourBuffer = null;
	
	setMatrixUniformsInShader_shared();
	gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, cubeVertexIndexBuffer);

	var zAxisDataPointArrays = model.getDataPointArrays();
	var dataPoints;
	
	// for each z index
	for(zIndex in zAxisDataPointArrays) {
	
		// determine if this z index is selected
		if(zIndex == model.getSelectedZIndex()) {
			isSelectedOnZAxis = true;
		} else {
			isSelectedOnZAxis = false;
		}
		
		// for each data point on the current z index
		dataPoints = zAxisDataPointArrays[zIndex];
		for(i in dataPoints) {
		
			mvPushMatrix(); // push for data point
			
			// translate for x and z index position
			mat4.translate(
				mvMatrix, 
				[
					(BAR_X_SPACING + BAR_X_WIDTH) * dataPoints[i][model.DATA_POINT_X_INDEX_INDEX], // x spacing * x pos
					dataPoints[i][model.DATA_POINT_Y_VALUE_INDEX] * 0.5,                           // y height / 2
					-(BAR_Z_SPACING + BAR_Z_WIDTH) * dataPoints[i][model.DATA_POINT_Z_INDEX_INDEX] // z spacing * z pos
				]);

			// scale for datapoint's y value
			mat4.scale(
				mvMatrix, 
				[
					BAR_X_WIDTH,                                   // x width
					dataPoints[i][model.DATA_POINT_Y_VALUE_INDEX], // y height
					BAR_Z_WIDTH                                    // z width
				]);

			// determine if current data pointis selected on it's x axis index
			if(dataPoints[i][model.DATA_POINT_X_INDEX_INDEX] == model.getSelectedXIndex()) {
				isSelectedOnXAxis = true;
			} else {
				isSelectedOnXAxis = false;
			}
			
			// determine colour using selection info
			if(isSelectedOnZAxis) {
				if(isSelectedOnXAxis) { // if selected on z and x
					currDataPointColourBuffer = cubeVertexColorBuffer_brightRed;
				} else { // if just selected on z
					currDataPointColourBuffer = cubeVertexColorBuffer_red;
				}
			} else { // else not selected on z or x axis
				currDataPointColourBuffer = cubeVertexColorBuffer_yellow;
			}
			
			// if current data point's color is different from the previous one drawn
			if(currDataPointColourBuffer != prevDataPointColourBuffer) {
				// bind relevant colour buffer
				gl.bindBuffer(gl.ARRAY_BUFFER, currDataPointColourBuffer);
				gl.vertexAttribPointer(shaderProgram.vertexColorAttribute, currDataPointColourBuffer.itemSize, gl.FLOAT, false, 0, 0);
			}

			prevDataPointColourBuffer = currDataPointColourBuffer;
			
			// set the uniforms which arent shared (mv matrix) in the shader
			setMatrixUniformsInShader_notShared();
			
			// draw element
			gl.drawElements(gl.TRIANGLES, cubeVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);

			mvPopMatrix();
		}
	}
	
	if(testIsProfiling()) {
		addProfileData('drawScene - graph',new Date().getTime() - profileStart);
	}

	
}


var lastTime = 0;
var timeOfLastFramerateUpdate = 0;

// ##################
function animate() {

	var timeNow = new Date().getTime();

	// if not first animate
	if (lastTime != 0) {
		// get amount of time elapsed since last animate
		var elapsed = timeNow - lastTime;

		// if it's been over 0.3 second since we updated framrate
		if(timeNow - timeOfLastFramerateUpdate > 600) {

			timeOfLastFramerateUpdate = timeNow;

			// update framerate displayed
			updateHTMLForFramerate(Math.floor(1000/elapsed));
		}

		// update for animations
		rotationPyramid += (90 * elapsed) / 1000.0;
		rotationCube -= (75 * elapsed) / 1000.0;
		
		
	}
	lastTime = timeNow;
}


// ##################
function tick() {
	requestAnimFrame(tick,canvas);
	drawScene();
	animate();
}


